//
//  LGSModule.m
//  LogicSim
//
//  Created by Jeremy Pereira on 11/04/2012.
//  Copyright (c) 2012 Jeremy Pereira. All rights reserved.
//

#import "LGSModule.h"
#import "LGSEdge.h"
#import "LGSError.h"
#import "LGSConnectionContext.h"
#import "LGSSplitter.h"
#import "LGSName.h"

NSString* const LGS_NODES_ARRAY_KEY = @"nodes";
NSString* const LGS_INPUT_LABELS_KEY = @"inputs";
NSString* const LGS_OUTPUT_LABELS_KEY = @"outputs";
NSString* const LGS_INPUT_MAPPINGS_KEY = @"inputMappings";
NSString* const LGS_OUTPUT_MAPPINGS_KEY = @"outputMappings";
NSString* const LGS_CONNECTIONS_KEY = @"connections";

@interface LGSModule ()

-(bool) validateNodes: (id) nodes
          inputLabels: (id) inputLabels
         outputLabels: (id) outputLabels
          connections: (id) connections
        inputMappings: (id) inputMappings
       outputMappings: (id) outputMappings
          		error: (NSError* __autoreleasing*) error;

-(void) replicateConnection: (LGSEdge*) foreignEdge;

-(void) reindex: (LGSName*) nameToReindex;

@end

@implementation LGSModule
{
@private
    NSMutableDictionary* nodes_;
    int nextUniqueId;
    NSMutableDictionary* inputMappings_;
    NSMutableDictionary* outputMappings_;
}

-(id) initWithName: (LGSName*) newName
{
    self = [super initWithName: newName];
    if (self != nil)
    {
        nodes_ = [[NSMutableDictionary alloc] init];
        inputMappings_ = [[NSMutableDictionary alloc] init];
        outputMappings_ = [[NSMutableDictionary alloc] init];
    }
    return self;
}

-(id) copyWithZone: (NSZone*) zone
{
    id ret = [super copyWithZone: zone];
    if (ret != nil)
    {
        NSMutableSet* requiredConnections = [[NSMutableSet alloc] init];
        for (id node in [nodes_ allValues])
        {
            LGSNode* newNode = [node copy];
            [ret addNode: newNode];
            for (LGSName* inputName in [node inputNames])
            {
                LGSEdge* anEdge = [node input: inputName];
                if (anEdge != [LGSEdge null])
                {
                    [requiredConnections addObject: anEdge];
                }
            }
        }
        /*
         *  We need to replicate the connections in the new node.
         */
        for (LGSEdge* anEdge in requiredConnections)
        {
            [ret replicateConnection: anEdge];
        }
        /*
         *  And the input mappings.
         */
        [inputMappings_ enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop)
        {
            NSAssert1([obj isKindOfClass: [LGSSplitter class]],
                      @"%@ must be a splitter",
                      obj);
            NSMutableArray* targets = [[NSMutableArray alloc] init];
            NSSet* mappingDestEdges = [obj outputsForName: [[obj class] outputName]];
            for (LGSEdge* edge in mappingDestEdges)
            {
                LGSName* targetName = [[[edge output] name] nameWithConnection: [edge outputName]];
                [targets addObject: targetName];
            }
            [ret setInputMappingFrom: key to: targets];
        }];
        /*
         *  And the output mappings
         */
        [outputMappings_ enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop)
         {
             [ret addOutputMappingFrom: key to: obj];
         }];
    }
    return ret;
}

-(void) replicateConnection: (LGSEdge*) foreignEdge
{
    LGSName* inputNodeName = [[foreignEdge input] name];
    LGSName* outputNodeName = [[foreignEdge output] name];
    LGSNode* inputNode = [self nodeForName: inputNodeName];
    LGSNode* outputNode = [self nodeForName: outputNodeName];
    /*
     *  There maybe connections from nodes to input splitters or from nodes
     *  to outputs.  If so, we can ignore the connection.
     */
    if (inputNode != nil && outputNode != nil)
    {
        LGSName* inputConnection = [[foreignEdge inputName] nameWithoutNode];
        LGSName* outputConnection = [[foreignEdge outputName] nameWithoutNode];
        if (![inputNode connect: outputNode from: inputConnection to: outputConnection])
        {
            @throw [NSException exceptionWithName: @"ModuleReplication"
                                           reason: [NSString stringWithFormat:
                                                    @"Inconsistent module, nodes %@ and %@ should be connectable",
                                                    inputNodeName,
                                                    outputNodeName]
                                         userInfo: nil];
        }        
    }
}


-(bool) overrideWithDictionary: (NSDictionary*) dictionary
                       context: (LGSConnectionContext*) context
                         error: (NSError* __autoreleasing*) error
{
    __block bool ret = [super overrideWithDictionary: dictionary
                                             context: context
                                               error: error];
    __block NSError* myError = nil;
    if (ret)
    {
        id nodes = [dictionary objectForKey: LGS_NODES_ARRAY_KEY];
        id inputLabels = [dictionary objectForKey: LGS_INPUT_LABELS_KEY];
        id outputLabels = [dictionary objectForKey: LGS_OUTPUT_LABELS_KEY];
        id connections = [dictionary objectForKey: LGS_CONNECTIONS_KEY];
        id inputMappings = [dictionary objectForKey: LGS_INPUT_MAPPINGS_KEY];
        id outputMappings = [dictionary objectForKey: LGS_OUTPUT_MAPPINGS_KEY];
        ret = [self validateNodes: nodes
                      inputLabels: inputLabels
                     outputLabels: outputLabels
                      connections: connections
                    inputMappings: inputMappings
                   outputMappings: outputMappings
                            error: error];
        if (ret)
        {
            [context push];
            [nodes enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop)
            {
                LGSNode* newNode = [LGSNode makeNodeFromDictionary: obj
                                                 connectionContext: context
                                                             error: &myError];
                if (newNode != nil)
                {
                    [self addNode: newNode];
                }
                else
                {
                    *stop = YES;
                    ret = false;
                }
            }];
            if (ret)
            {
                [self setInputNamesFromStrings: inputLabels];
                [self setOutputNamesFromStrings: outputLabels];
                ret = [LGSNode createConnections: connections
                                    usingContext: context
                                           error: &myError];
            }
            if (ret)
            {
                [inputMappings_ removeAllObjects];
                [inputMappings enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop)
                {
                    if (![self setInputMappingFrom: [LGSName nameWithConnectionString: key]
                                                to: [obj valueForKey: @"asName"]])
                    {
                        *stop = YES;
                        ret = false;
                        NSString* reason = [NSString stringWithFormat: @"input mapping %@ to %@ incorrect",
                                            key, obj];
                        myError = [LGSError deserialisationFailureReason: reason];
                    }
                }];
            }
            if (ret)
            {
                [outputMappings_ removeAllObjects];
                [outputMappings enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop)
                 {
                     if (![self addOutputMappingFromString: key toString: obj])
                     {
                         *stop = YES;
                         ret = false;
                         NSString* reason = [NSString stringWithFormat: @"output mapping %@ to %@ incorrect",
                                             key, obj];
                         myError = [LGSError deserialisationFailureReason: reason];
                     }
                 }];
            }
        	if (!ret && error != nil)
            {
                *error = myError;
            }

    		[context pop];
        }
    }
    return ret;
}

-(void) addNodesFromSet: (NSSet*) setOfNodes
{
    for (LGSNode* node in setOfNodes)
    {
        [self addNode: node];
    }
}

-(bool) canModifyConnections
{
    return true;
}

-(bool) addInput:(LGSEdge*) newInput forName:(NSString *)inputName
{
    bool ret = false;
    LGSSplitter* realInputs = [inputMappings_ objectForKey: inputName];
    if (realInputs != nil)
    {
        ret = [realInputs addInput: newInput
                           forName: [[realInputs class] inputName]];
    }
    return ret;
}

-(bool) addOutput:(LGSEdge*) newOutput forName: (LGSName*) outputName
{
    bool ret = false;
    LGSName* realOutput = [outputMappings_ objectForKey: [outputName nameWithoutNode]];
    NSAssert1([realOutput isKindOfClass: [LGSName class]],
              @"Output mapping is of wrong type %@",
              realOutput);
    if (realOutput != nil)
    {
        LGSNode* node = [self nodeForName: realOutput];
        if (node != nil)
        {
            ret = [node addOutput: newOutput forName: [realOutput nameWithoutNode]];
            if (!ret)
            {
                @throw [NSException exceptionWithName: @"InvalidOutput"
                                               reason: @"Output mappings should already have been checked"
                                             userInfo: nil];
            }
        }
    }
    else
    {
        @throw [NSException exceptionWithName: @"InvalidOutput"
                                       reason: @"Output mappings should already have been checked"
                                     userInfo: nil];
    }
    return ret;
}

-(bool) setInputMappingFrom: (LGSName*) inputName to: (NSArray*) internalInputs
{
    __block bool ret = [self hasInput: inputName];
    
    if (ret)
    {
        LGSName* splitterName
        	= [LGSName nameWithString: [NSString stringWithFormat:
                                        @"input %@",
                                        inputName]];
        LGSSplitter* mappingSplit = [[LGSSplitter alloc] initWithName: splitterName];
        [mappingSplit setIsTransient: true];
        
        [internalInputs enumerateObjectsUsingBlock:^(id aName, NSUInteger idx, BOOL *stop)
        {
            if (aName == nil)
            {
                *stop = YES;
                ret = false;
            }
            else
            {
                LGSNode* aNode = [self nodeForName: aName];
                if (aNode == nil)
                {
                    *stop = YES;
                    ret = false;
                }
                else if (![mappingSplit connect: aNode
                                           from: [[mappingSplit class] outputName]
                                       		 to: [aName nameWithoutNode]])
                {
                    *stop = YES;
                    ret = false;
                }
            }
        }];
        if (ret)
        {
            [inputMappings_ setObject: mappingSplit forKey: inputName];
        }
    }
    
    return ret;
}

-(bool) setInputMappingFromString: (NSString*) inputNameString
                        toStrings: (NSArray*) internalInputStrings
{
    LGSName* inputName = [LGSName nameWithConnectionString: inputNameString];
    NSMutableArray* internalNames = [[NSMutableArray alloc] init];
    for (NSString* nameString in internalInputStrings)
    {
        [internalNames addObject: [LGSName nameWithString: nameString]];
    }
    return [self setInputMappingFrom: inputName to: internalNames];
}


-(bool) addInputMappingFrom: (LGSName*) inputName
                 toNodeName: (LGSName*) nodeNameWithConnection
{
    bool ret = false;
    
    LGSNode* destNode = [self nodeForName: nodeNameWithConnection];
    
    if ([self hasInput: inputName]
        && destNode != nil
        && [destNode hasInput: [nodeNameWithConnection nameWithoutNode]])
    {
        LGSSplitter* splitter = [inputMappings_ objectForKey: inputName];
        if (splitter == nil)
        {
            /*
             *  No input mappings have been set up yet for this name.  We use 
             *  setInputMappingFrom:to:  to set everything up.  Probably should
             *  work the other way around, actually.
             */
            NSArray* internalMappings
            	= [NSArray arrayWithObject: [nodeNameWithConnection stringValue]];
            [self setInputMappingFrom: inputName to: internalMappings];
        }
        else
        {
            [splitter connect: destNode
                         from: [[splitter class] outputName]
                           to: [nodeNameWithConnection nameWithoutNode]];
        }
        ret = true;
    }
    return ret;
}


-(NSArray*) inputMappingsFor: (NSString*) inputName
{
    static dispatch_once_t onceToken;
    static NSArray* sortDescriptors = nil;
    dispatch_once(&onceToken,
    ^{
        sortDescriptors
        	= [NSArray arrayWithObject: [NSSortDescriptor sortDescriptorWithKey: @"self"
                                                                      ascending: YES
                                                                       selector: @selector(localizedCompare:)]];
    });
    
	LGSSplitter* splitter = [inputMappings_ objectForKey: inputName];
    NSSet* outputs = [splitter outputsForName: [[splitter class] outputName]];
    NSSet* outputNames = [outputs valueForKey: @"outputName"];
    NSArray* ret = [outputNames sortedArrayUsingDescriptors: sortDescriptors];
    return ret;
}



-(bool) addOutputMappingFrom: (LGSName*) outputName to: (LGSName*) internalOutput
{
    bool ret = [self hasOutput: outputName];
    
    if (ret)
    {
        LGSNode* aNode = [self nodeForName: internalOutput];
        if (aNode == nil || ![aNode hasOutput: [internalOutput nameWithoutNode]])
        {
             ret = false;
        }
        else
        {
            [outputMappings_ setObject: internalOutput forKey: outputName];
        }
    }
    
    return ret;
}

-(bool) addOutputMappingFromString: (NSString*) outputNameString
                          toString: (NSString*) internalOutputString
{
    LGSName* outputName = [LGSName nameWithConnectionString: outputNameString];
    LGSName* internalOutput = [LGSName nameWithString: internalOutputString];
    return [self addOutputMappingFrom: outputName to: internalOutput];
}

-(void) addJsonKeysToDictionary: (NSMutableDictionary*) dictionary
{
    [super addJsonKeysToDictionary: dictionary];
    NSMutableArray* nodesForJson = [[NSMutableArray alloc] init];
    for (LGSNode* aNode in [nodes_ allValues])
    {
        NSDictionary* nodeDict = [aNode jsonify];
        [nodesForJson addObject: nodeDict];
    }
    [dictionary setObject: nodesForJson forKey: LGS_NODES_ARRAY_KEY];
    NSMutableDictionary* requiredConnections = [[NSMutableDictionary alloc] init];
    [dictionary setObject: [[self inputNames] valueForKey: @"connectionPart"]
                   forKey: LGS_INPUT_LABELS_KEY];
    [dictionary setObject: [[self outputNames] valueForKey: @"connectionPart"]
                   forKey: LGS_OUTPUT_LABELS_KEY];
    for (LGSNode* node in [nodes_ allValues])
    {
        for (LGSName* inputName in [node inputNames])
        {
            LGSEdge* anEdge = [node input: inputName];
            if (anEdge != [LGSEdge null]
                && ![[anEdge input] isTransient])
            {
                LGSNode* edgeInputNode = [anEdge input];
                if (edgeInputNode != nil && [edgeInputNode name] != [LGSName null])
                {
                    NSString* inputString = [NSString stringWithFormat: @"%@.%@",
                                             [[edgeInputNode name] nodePart],
                                             [[anEdge inputName] connectionPart]];
                    NSString* outputString = [NSString stringWithFormat: @"%@.%@",
                                              [[node name] nodePart],
                                              [[anEdge outputName] connectionPart]];
                    [requiredConnections setObject: inputString
                                            forKey: outputString];
                }
                
            }
        }
    }
    [dictionary setObject: requiredConnections forKey: LGS_CONNECTIONS_KEY];
    
    NSMutableDictionary* inputMappings = [[NSMutableDictionary alloc] init];
    
    for (LGSName* inputName in [self inputNames])
    {
        NSArray* inputMappingNames = [self inputMappingsFor: inputName];
        if (inputMappingNames != nil)
        {
            NSArray* targets = [inputMappingNames valueForKey: @"stringValue"];
            [inputMappings setObject: targets forKey: [inputName connectionPart]];
        }
        
    }
    
    [dictionary setObject: inputMappings forKey: LGS_INPUT_MAPPINGS_KEY];
    
    NSMutableDictionary* outputMappings = [[NSMutableDictionary alloc] init];
    [outputMappings_ enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop)
    {
        [outputMappings setObject: [obj stringValue] forKey: [key connectionPart]];
    }];
    [dictionary setObject: outputMappings forKey: LGS_OUTPUT_MAPPINGS_KEY];

}

-(void) addNode: (LGSNode*) newNode
{
    NSAssert1([newNode module] == nil,
              @"Adding node to a module when it is already in one, %@", 
              newNode);
    NSAssert1([newNode name] != nil,
              @"Adding node to a module whith no name, %@",
              newNode);
    bool needReindex = false;
    if ([nodes_ objectForKey: [newNode name]] != nil)
    {
        needReindex = true;
        [newNode setName: [[newNode name] nameWithNodeIndex: LGS_NAME_PLACEHOLDER_INDEX]];
    }
    [nodes_ setObject: newNode forKey: [newNode name]];
    [newNode setModule: self];
    if (needReindex)
    {
        [self reindex: [[newNode name] nameWithoutNodeIndex]];
    }
}

-(void) reindex: (LGSName*) nameToReindex
{
    NSMutableArray* nodesToReindex = [[NSMutableArray alloc] init];
    
    for (LGSName* candidate in [self nodeNames])
    {
        if ([[candidate nameWithoutNodeIndex] isEqual: [nameToReindex nameWithoutNodeIndex]])
        {
            [nodesToReindex addObject: [self nodeForName: candidate]];
        }
    }
    if ([nodesToReindex count] > 1)
    {
        int index = 0;
        /*
         *  Have to remove all the nodes first in case the new index range 
         *  overlaps the old index range
         */
        for (LGSNode* node in nodesToReindex)
        {
            [nodes_ removeObjectForKey: [node name]];
            [node setModule: nil];
            [node setName: [[node name] nameWithNodeIndex: index]];
            index++;
        }
        for (LGSNode* node in nodesToReindex)
        {
            [nodes_ setObject: node forKey: [node name]];
            [node setModule: self];
        }
    }
    else if ([nodesToReindex count] == 1)
    {
        LGSNode* theNode = [nodesToReindex objectAtIndex: 0];
        [nodes_ removeObjectForKey: theNode];
        [theNode setModule: nil];
        [theNode setName: [[theNode name] nameWithoutNodeIndex]];
        [theNode setModule: self];
    }
    else
    {
        NSLog(@"%s:%d: Should not be able to get here", __FILE__, __LINE__);
    }
}

-(void) removeNode: (LGSNode*) nodeToGo
{
    LGSName* nameOfNodeToGo = [nodeToGo name];
    if ([self nodeForName: nameOfNodeToGo] != nil)
    {
        [nodes_ removeObjectForKey: nameOfNodeToGo];
        [nodeToGo removeInputs];
        [nodeToGo removeOutputs];
        // TODO: Remove the mappings too
        if ([nameOfNodeToGo nodeIndex] != LGS_NAME_NO_INDEX)
        {
            [self reindex: [nameOfNodeToGo nameWithoutNodeIndex]];            
        }
    }
}


-(bool) canHaveSubNodes
{
    return true;
}

-(size_t) subNodeCount
{
    return [nodes_ count];
}


-(NSArray*) nodeNames
{
    return [[nodes_ allKeys] sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2)
    {
        return [obj1 localizedCompare: obj2];
    }];
}


-(LGSNode*) nodeForName: (LGSName*) name
{
    return [nodes_ objectForKey: [name nameWithoutConnection]];
}

-(bool) validateNodes: (id) nodes
          inputLabels: (id) inputLabels
         outputLabels: (id) outputLabels
          connections: (id) connections
        inputMappings: (id) inputMappings
       outputMappings: (id) outputMappings
          		error: (NSError* __autoreleasing*) error
{
    __block bool ret = [nodes isKindOfClass: [NSArray class]];
    
    if (!ret && error != nil)
    {
        NSString* message = [NSString stringWithFormat:
                             NSLocalizedString(@"Module %@ has invalid nodes", @"error"),
                             [self name]];
        *error = [LGSError deserialisationFailureReason: message];
    }
    
    if (ret)
    {
        ret = [inputLabels isKindOfClass: [NSArray class]];
        if (ret)
        {
            [inputLabels enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop)
            {
                if (![obj isKindOfClass: [NSString class]])
                {
                    *stop = YES;
                    ret = false;
                }
            }];
        }
        if (!ret && error != nil)
        {
            NSString* message = [NSString stringWithFormat:
                                 NSLocalizedString(@"Module %@ has invalid input labels", @"error"),
                                 [self name]];
            *error = [LGSError deserialisationFailureReason: message];
        }
    }
    
    if (ret)
    {
        ret = [outputLabels isKindOfClass: [NSArray class]];
        if (ret)
        {
            [outputLabels enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop)
             {
                 if (![obj isKindOfClass: [NSString class]])
                 {
                     *stop = YES;
                     ret = false;
                 }
             }];
        }
        if (!ret && error != nil)
        {
            NSString* message = [NSString stringWithFormat:
                                 NSLocalizedString(@"Module %@ has invalid output labels", @"error"),
                                 [self name]];
            *error = [LGSError deserialisationFailureReason: message];
        }
    }
    
    if (ret)
    {
        ret = [connections isKindOfClass: [NSDictionary class]];
        if (ret)
        {
            [connections enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) 
             {
                 if (![obj isKindOfClass: [NSString class]]
                     || ![key isKindOfClass: [NSString class]])
                 {
                     *stop = YES;
                     ret = false;
                 }
             }];            
        }
        if (!ret && error != nil)
        {
            NSString* message = [NSString stringWithFormat:
                                 NSLocalizedString(@"Module %@ has invalid connections", @"error"),
                                 [self name]];
            *error = [LGSError deserialisationFailureReason: message];
        }
    }
    
    if (ret)
    {
        ret = [outputMappings isKindOfClass: [NSDictionary class]];
        if (ret)
        {
            [outputMappings enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop)
             {
                 if (![obj isKindOfClass: [NSString class]]
                     || ![key isKindOfClass: [NSString class]])
                 {
                     *stop = YES;
                     ret = false;
                 }
             }];
        }
        if (!ret && error != nil)
        {
            NSString* message = [NSString stringWithFormat:
                                 NSLocalizedString(@"Module %@ has invalid output mappings", @"error"),
                                 [self name]];
            *error = [LGSError deserialisationFailureReason: message];
        }
    }
    
    if (ret)
    {
        ret = [inputMappings isKindOfClass: [NSDictionary class]];
        if (ret)
        {
            [inputMappings enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop)
             {
                 if (![obj isKindOfClass: [NSArray class]]
                     || ![key isKindOfClass: [NSString class]])
                 {
                     *stop = YES;
                     ret = false;
                 }
                 else
                 {
                     [obj enumerateObjectsUsingBlock:^(id obj2, NSUInteger idx, BOOL *stop2)
                     {
                         if (![obj2 isKindOfClass: [NSString class]])
                         {
                             *stop2 = YES;
                             ret = false;
                         }
                     }];
                     if (!ret)
                     {
                         *stop = YES;
                     }
                 }
             }];
        }
        if (!ret && error != nil)
        {
            NSString* message = [NSString stringWithFormat:
                                 NSLocalizedString(@"Module %@ has invalid output mappings", @"error"),
                                 [self name]];
            *error = [LGSError deserialisationFailureReason: message];
        }
    }
    
    return ret;
}

-(void) changeNameForNode: (LGSNode*) theNode
{
    NSArray* oldNames = [nodes_ allKeysForObject: theNode];
    if ([oldNames count] != 1)
    {
        NSString* reason = [NSString stringWithFormat: @"Node rename: module %@ cannot uniquely determine old node name",
                            self];
        @throw [NSException exceptionWithName: @"Inconsistency"
                                       reason: reason
                                     userInfo: nil];
    }
    LGSName* oldName = [oldNames objectAtIndex: 0];
    LGSName* newName = [theNode name];
    
    if ([self nodeForName: newName] != nil)
    {
        NSString* reason = [NSString stringWithFormat: @"Node rename: module %@ already has a node with the target name %@",
                            self,
                            newName];
        @throw [NSException exceptionWithName: @"Inconsistency"
                                       reason: reason
                                     userInfo: nil];
    }
	[nodes_ removeObjectForKey: oldName];
    [nodes_ setObject: theNode forKey: newName];
}

@end
